home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC World Komputer 2010 April
/
PCWorld0410.iso
/
pluginy Firefox
/
10900
/
10900.xpi
/
modules
/
service.js
< prev
next >
Wrap
Text File
|
2010-01-12
|
51KB
|
1,376 lines
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Personas.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chris Beard <cbeard@mozilla.org>
* Myk Melez <myk@mozilla.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
let EXPORTED_SYMBOLS = ["PersonaService", "PERSONAS_EXTENSION_ID"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
// modules that come with Firefox
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// LightweightThemeManager may not be not available (Firefox < 3.6 or Thunderbird)
try { Cu.import("resource://gre/modules/LightweightThemeManager.jsm"); }
catch (e) { LightweightThemeManager = null; }
// modules that are generic
Cu.import("resource://personas/modules/JSON.js");
Cu.import("resource://personas/modules/Observers.js");
Cu.import("resource://personas/modules/Preferences.js");
Cu.import("resource://personas/modules/StringBundle.js");
Cu.import("resource://personas/modules/URI.js");
const PERSONAS_EXTENSION_ID = "personas@christopher.beard";
const COOKIE_INITIAL_PERSONA = "initial_persona";
const COOKIE_USER = "PERSONA_USER";
let PersonaService = {
THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}",
FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
//**************************************************************************//
// Shortcuts
// Access to extensions.personas.* preferences. To access other preferences,
// call the Preferences module directly.
get _prefs() {
delete this._prefs;
return this._prefs = new Preferences("extensions.personas.");
},
get _strings() {
delete this._strings;
return this._strings = new StringBundle("chrome://personas/locale/personas.properties");
},
get appInfo() {
delete this.appInfo;
return this.appInfo = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo).
QueryInterface(Ci.nsIXULRuntime);
},
get extension() {
delete this.extension;
if (this.appInfo.ID == this.FIREFOX_ID) {
return this.extension = Cc["@mozilla.org/fuel/application;1"].
getService(Ci.fuelIApplication).
extensions.get(PERSONAS_EXTENSION_ID);
}
// If STEEL provides a FUEL-compatible extIExtension interface
// in Thunderbird, return it here.
return this.extension = null;
},
//**************************************************************************//
// Initialization & Destruction
_init: function() {
// Observe quit so we can destroy ourselves.
Observers.add("quit-application", this.onQuitApplication, this);
// Observe the "cookie-changed" topic to load the favorite personas when
// the user signs in.
Observers.add("cookie-changed", this.onCookieChanged, this);
// Observe the "lightweight-theme-changed" to sync the add-on with the
// lightweight theme manager.
Observers.add("lightweight-theme-changed",
this.onLightweightThemeChanged, this);
this._prefs.observe("useTextColor", this.onUseColorChanged, this);
this._prefs.observe("useAccentColor", this.onUseColorChanged, this);
this._prefs.observe("selected", this.onSelectedModeChanged, this);
// Get the initial persona specified by a cookie, if any. The gallery
// sets this when users download Personas from the Details page
// for a specific persona, so that the user sees that persona when they
// install the extension. We only check for a cookie on first run,
// because it would be expensive to traverse cookies every time, and since
// the gallery only sets the initial persona cookie on installation.
let personaFromCookie;
if (this.extension && this.extension.firstRun)
personaFromCookie = this._getPersonaFromCookie();
// Change to the initial persona if preferences indicate that a persona
// should be active but they don't specify the persona that is active.
// This normally happens only on first run, although it could theoretically
// happen at other times.
//
// The initial persona is either the persona specified by a cookie (set by
// the gallery), the one specified by the extensions.personas.initial
// preference (set by a distribution.ini file for a BYOB/bundle/distributon
// build), or the Groovy Blue persona which is hardcoded into this code.
//
// NOTE: the logic here is carefully designed to achieve the correct outcome
// under a variety of circumstances and should be changed with caution!
// See bug 513765 and bug 503300 for some details on why it works this way.
//
if (this._prefs.get("selected") == "current" && !this._prefs.get("current")) {
if (personaFromCookie)
this.changeToPersona(personaFromCookie);
else if (this._prefs.has("initial"))
this.changeToPersona(JSON.parse(this._prefs.get("initial")));
else {
this.changeToPersona({
"id":"33",
"name":"Groovy Blue",
"accentcolor":"#6699ff",
"textcolor":"#07188d",
"header":"3\/3\/33\/tbox-groovy_blue.jpg",
"footer":"3\/3\/33\/stbar-groovy_blue.jpg",
"category":"Abstract",
"description":null,
"author":"Lee.Tom",
"username":"Lee.Tom",
"detailURL":"http:\/\/www.getpersonas.com\/persona\/33",
"headerURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/tbox-groovy_blue.jpg",
"footerURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/stbar-groovy_blue.jpg",
"previewURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/preview.jpg",
"iconURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/preview_small.jpg",
"dataurl":"data:image\/png;base64,\/9j\/4AAQSkZJRgABAQEASABIAAD\/2" +
"wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQIC" +
"AQECAQEBAgICAgICAgICAQICAgICAgICAgL\/2wBDAQEBAQEBAQEBAQ" +
"ECAQEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA" +
"gICAgICAgICAgICAgL\/wAARCAAQABADAREAAhEBAxEB\/8QAFwAAAw" +
"EAAAAAAAAAAAAAAAAABAcICv\/EACAQAAIDAAEFAQEAAAAAAAAAAAME" +
"AQIFBgcIERIjEyX\/xAAZAQACAwEAAAAAAAAAAAAAAAADCAQFBwn\/x" +
"AAlEQACAQMCBgMBAAAAAAAAAAABAgMEERIAIQUGExQiMQcyM1L\/2gA" +
"MAwEAAhEDEQA\/AL9p21L52peGctNVXkGbWsGZhNZlXBWo0sHVSVucA" +
"9BryNibVm9Iq3ahWLengV+g03yTMyl+6aV1YE45YmTZsGsCVxBUjx+p" +
"sovuFgi5IjlgRUhI7RjYHyJy93Y5EKxBW+9wrKN9wjsft65bGnnPK4+" +
"kTguZqqKcdJstEzbuzyTdErUtSFqszqY4uR7lQGdQ+q13rML1vWIrN1" +
"U\/I1N2FTG6oKipx6pUlijxRM+ICsRE8scbuqSDEqCvsAiRJyNRvUBH" +
"qcq9I5XdFTxaNbKcmxdWMcYRSt\/IqokK5a0NdQOicaI0cEnGi6eWiW" +
"TKvqghltgpT1GECpZj+DYQmXSG97SAtxBkXibk9Ul4TzXMk3dSV6wMg" +
"a6MSDshAuB+xYiNQVs4F8jZV1u9RwmCKGdKWneSorMFJGPTQGRWkYlj" +
"ZVAyKqQRckAG+i+M9rHGtTUI31CU3dwGWRG+GN7lTWmhe4VpCxNRnr+" +
"y47Wm9qxSwigg5A0YJT6yDifOtS0MS8Km6JmDdZelgASbgeyrEfUtdg" +
"4VXKoSU0bhlDJStUxz00KgWWORAuboP6IAIBIywKgKSRd7Btf\/2Q=="
});
}
}
let timerManager = Cc["@mozilla.org/updates/timer-manager;1"].
getService(Ci.nsIUpdateTimerManager);
// Load cached personas data
this.loadDataFromCache();
// Refresh data, then set a timer to refresh it periodically.
// This isn't quite right, since we always load data on startup, even if
// we've recently refreshed it. And the timer that refreshes data ignores
// the data load on startup, so if it's been more than the timer interval
// since a user last started her browser, we load the data twice:
// once because the browser starts and once because the refresh timer fires.
this.refreshData();
let dataRefreshCallback = {
_svc: this,
notify: function(timer) { this._svc._refreshDataWithMetrics() }
};
timerManager.registerTimer("personas-data-refresh-timer",
dataRefreshCallback,
86400 /* in seconds == one day */);
// Refresh the current persona once per day. We only do this for
// Thunderbird, since Firefox's built-in LightweightThemeManager does this
// for Firefox nowadays.
if (this.appInfo.ID == this.THUNDERBIRD_ID) {
let personaRefreshCallback = {
_svc: this,
notify: function(timer) { this._svc._refreshPersona() }
};
timerManager.registerTimer("personas-persona-refresh-timer",
personaRefreshCallback,
86400 /* in seconds == one day */);
}
// Load cached favorite personas
this.loadFavoritesFromCache();
this.refreshFavorites();
// Refresh the favorite personas once per day.
let favoritesRefreshCallback = {
_svc: this,
notify: function(timer) { this._svc.refreshFavorites() }
};
timerManager.registerTimer("personas-favorites-refresh-timer",
favoritesRefreshCallback,
86400 /* in seconds == one day */);
this._rotationTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.onSelectedModeChanged();
},
_destroy: function() {
Observers.remove("cookie-changed", this.onCookieChanged, this);
Observers.remove("lightweight-theme-changed",
this.onLightweightThemeChanged, this);
this._prefs.ignore("useTextColor", this.onUseColorChanged, this);
this._prefs.ignore("useAccentColor", this.onUseColorChanged, this);
this._prefs.ignore("selected", this.onSelectedModeChanged, this);
},
//**************************************************************************//
// XPCOM Plumbing
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsIDOMEventListener,
Ci.nsITimerCallback]),
//**************************************************************************//
// Data Retrieval
_makeRequest: function(url, loadCallback, headers, aIsBinary) {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
request = request.QueryInterface(Ci.nsIDOMEventTarget);
request.addEventListener("load", loadCallback, false);
request = request.QueryInterface(Ci.nsIXMLHttpRequest);
request.open("GET", url, true);
// Force the request to include cookies even though this chrome code
// is seen as a third-party, so the server knows the user for which we are
// requesting favorites (or anything else user-specific in the future).
// This only works in Firefox 3.6; in Firefox 3.5 the request will instead
// fail to send cookies if the user has disabled third-party cookies.
try {
request.channel.QueryInterface(Ci.nsIHttpChannelInternal).
forceAllowThirdPartyCookie = true;
}
catch(ex) { /* user is using Firefox 3.5 */ }
if (headers)
for (let header in headers)
request.setRequestHeader(header, headers[header]);
if (aIsBinary)
request.overrideMimeType('text/plain; charset=x-user-defined');
request.send(null);
},
/**
* Refresh data. This method gets called on demand (including on startup)
* and retrieves data without passing any additional information about
* the selected persona and the application (that information is only included
* in the daily retrieval so we can get consistent daily statistics from it
* no matter how many times a user starts the application in a given day).
*/
refreshData: function() {
let url = this.dataURL + "index_" + this._prefs.get("data.version") + ".json";
let t = this;
this._makeRequest(url, function(evt) { t.onDataLoadComplete(evt) });
},
/**
* Refresh data, providing metrics on persona usage in the process.
* This method gets called approximately once per day on a cross-session timer
* (provided Firefox is run every day), updates the version of the data
* that is currently in memory, and passes information about the selected
* persona and the host application to the server for statistical analysis
* (f.e. figuring out which personas are the most popular).
*/
_refreshDataWithMetrics: function() {
let appInfo = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo);
let xulRuntime = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULRuntime);
// Calculate the amount of time (in hours) since the persona was last changed.
let duration = "";
if (this._prefs.has("persona.lastChanged"))
duration = Math.round((new Date() - new Date(parseInt(this._prefs.get("persona.lastChanged")))) / 1000 / 60 / 60);
// This logic is based on ExtensionManager::_updateLocale.
let locale;
try {
if (Preferences.get("intl.locale.matchOS")) {
let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"].
getService(Ci.nsILocaleService);
locale = localeSvc.getLocaleComponentForUserAgent();
}
else
throw "set locale in the catch block";
}
catch (ex) {
locale = Preferences.get("general.useragent.locale");
}
let params = {
type: this.selected,
id: this.currentPersona ? this.currentPersona.id : "",
duration: duration,
appID: appInfo.ID,
appVersion: appInfo.version,
appLocale: locale,
appOS: xulRuntime.OS,
appABI: xulRuntime.XPCOMABI
};
//dump("params: " + [name + "=" + encodeURIComponent(params[name]) for (name in params)].join("&") + "\n");
let url = this.dataURL + "index_" + this._prefs.get("data.version") + ".json?" +
[name + "=" + encodeURIComponent(params[name]) for (name in params)].join("&");
let t = this;
this._makeRequest(url, function(evt) { t.onDataLoadComplete(evt) });
},
onDataLoadComplete: function(aEvent) {
let request = aEvent.target;
// XXX Try to reload again sooner?
if (request.status != 200)
throw("problem loading data: " + request.status + " - " + request.statusText);
this.personas = JSON.parse(request.responseText);
// Cache the response
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
FileUtils.writeFile(cacheDirectory, "personas.json", request.responseText);
},
/**
* Attempts to load this.personas from the cached file in cache/personas.json
*/
loadDataFromCache : function() {
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
let data = FileUtils.readFile(cacheDirectory, "personas.json");
try { this.personas = JSON.parse(data); }
catch (e) {
// Could not load from cached data, file empty or does not exist perhaps
return;
}
// Now that we have data, pick a new random persona. Currently, this is
// the only time we pick a random persona besides when the user selects
// the "Random From [category]" menuitem, which means the user gets a new
// random persona each time they start the browser.
if (this.selected == "random") {
this.currentPersona = this._getRandomPersonaFromCategory(this.category);
this._prefs.reset("persona.lastRefreshed");
this._notifyPersonaChanged(this.currentPersona);
}
},
/**
* Makes a request to obtain the favorite personas json. This occurs only if
* a user is currenly signed in.
*/
refreshFavorites : function() {
// Only refresh if the user is signed in at the moment.
if (this.isUserSignedIn) {
let url =
"http://" + this._prefs.get("host") + "/gallery/All/Favorites?json=1";
let t = this;
this._makeRequest(url, function(evt) { t.onFavoritesLoadComplete(evt) });
}
},
/**
* Handles the response from the refreshFavorites method. Loads the favorite
* personas list.
* @param aEvent The Http request event object.
*/
onFavoritesLoadComplete : function(aEvent) {
let request = aEvent.target;
if (request.status != 200)
throw("problem loading favorites: " + request.status + " - " + request.statusText);
try {
this.favorites = JSON.parse(request.responseText);
}
catch(ex) {
Cu.reportError("error parsing favorites data; perhaps you are using " +
"Firefox 3.5 and have disabled third-party cookies?");
return;
}
// Cache the response
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
FileUtils.writeFile(cacheDirectory, "favorites.json", request.responseText);
},
/**
* Attempts to load this.favorites from the cached file in
* cache/favorites.json
*/
loadFavoritesFromCache : function() {
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
let data = FileUtils.readFile(cacheDirectory, "favorites.json");
try { this.favorites = JSON.parse(data); }
catch (e) {
// Could not load from cached data, file empty or does not exist perhaps
}
},
/**
* Adds the given persona to the favorites list. If the persona is already
* in the list then it is replaced.
* @param aPersona The persona object to be added.
*/
addFavoritePersona : function(aPersona) {
// Make sure the favorites list exists.
if (!this.favorites)
this.favorites = [];
let i = this._findPersonaInArray(aPersona, this.favorites);
if (i >= 0)
this.favorites[i] = aPersona;
else
this.favorites.push(aPersona);
},
/**
* Removes the given persona from the favorites list, if found.
* @param aPersona The persona object to be removed.
*/
removeFavoritePersona : function(aPersona) {
// Abort if the favorites list hasn't been created.
if (!this.favorites)
return;
let i = this._findPersonaInArray(aPersona, this.favorites);
if (i >= 0)
this.favorites.splice(i, 1);
},
_refreshPersona: function() {
// Only refresh the persona if the user selected a specific persona with an
// ID and update URL. If the user selected a random persona, we'll change
// it the next time we refresh the directory; if the user selected
// the default persona, we don't need to refresh it, as it doesn't change;
// if the user selected a custom persona (which doesn't have an ID), it's
// not clear what refreshing it would mean; and if the persona doesn't have
// an update URL, then we don't have a way to refresh it.
if (this.selected != "current" ||
!this.currentPersona ||
!this.currentPersona.id ||
!this.currentPersona.updateURL)
return;
let headers = {};
if (this._prefs.has("persona.lastRefreshed")) {
let date = new Date(parseInt(this._prefs.get("persona.lastRefreshed")));
headers["If-Modified-Since"] = DateUtils.toRFC1123(date);
}
let t = this;
this._makeRequest(this.currentPersona.updateURL,
function(evt) { t.onPersonaLoadComplete(evt) },
headers);
},
onPersonaLoadComplete: function(event) {
let request = event.target;
// 304 means the file we requested has not been modified since the
// If-Modified-Since date we specified, so there's nothing to do.
if (request.status == 304) {
//dump("304 - the persona has not been modified\n");
return;
}
if (request.status != 200)
throw("problem refreshing persona: " + request.status + " - " + request.statusText);
let persona = JSON.parse(request.responseText);
// If the persona we're refreshing is no longer the selected persona,
// then cancel the refresh (otherwise we'd undo whatever changes the user
// has just made).
if (this.selected != "current" || !this.currentPersona ||
this.currentPersona.id != persona.id) {
//dump("persona " + persona.id + "(" + persona.name + ") no longer the current persona; ignoring refresh\n");
return;
}
// If the version strings are identical, the persona hasn't changed.
if ((persona.version || "") == (this.currentPersona.version || ""))
return;
// Set the current persona to the updated version we got from the server,
// and notify observers about the change.
this.currentPersona = persona;
this._notifyPersonaChanged(this.currentPersona);
// Record when this refresh took place so the next refresh only looks
// for changes since this refresh.
// Note: we set the preference to a string value because preferences
// can't hold large enough integer values.
this._prefs.set("persona.lastRefreshed", new Date().getTime().toString());
},
//**************************************************************************//
// Implementation
// The JSON feed of personas retrieved from the server.
// Loaded upon service initialization and reloaded periodically thereafter.
personas: null,
// The JSON feed of favorite personas retrieved from the server.
favorites: null,
/**
* extensions.personas.selected: the type of persona that the user selected;
* possible values are default (the default Firefox theme), random (a random
* persona from a category), current (the value of this.currentPersona), and
* randomFavorite (a random persona from the favorite list).
*/
get selected() { return this._prefs.get("selected") },
set selected(newVal) { this._prefs.set("selected", newVal) },
/**
* extensions.personas.current: the current persona
*/
get currentPersona() {
let current = this._prefs.get("current");
if (current) {
try { return JSON.parse(current) }
catch(ex) { Cu.reportError("error getting current persona: " + ex) }
}
return null;
},
set currentPersona(newVal) {
try {
this._prefs.set("current", JSON.stringify(newVal));
this._cachePersonaImages(newVal);
}
catch(ex) { Cu.reportError("error setting current persona: " + ex) }
},
/**
* extensions.personas.category: the category from which to pick a random
* persona.
*/
get category() { return this._prefs.get("category") },
set category(newVal) { this._prefs.set("category", newVal) },
/**
* The URL at which the static data is located.
*/
get dataURL() {
return "http://" + this._prefs.get("host") + "/static/";
},
/**
* extensions.personas.custom: the custom persona.
*/
get customPersona() {
let custom = this._prefs.get("custom");
if (custom) {
try { return JSON.parse(custom) }
catch(ex) { Cu.reportError("error getting custom persona: " + ex) }
}
return null;
},
set customPersona(newVal) {
try { this._prefs.set("custom", JSON.stringify(newVal)) }
catch(ex) { Cu.reportError("error setting custom persona: " + ex) }
},
/**
* Notifies the persona changes or uses the lightweight theme manager
* functionality for this purpose (if available)
* @param aPersona the persona to be set as current if the lightweight theme
* manager is available
*/
_notifyPersonaChanged : function(aPersona) {
if (LightweightThemeManager)
LightweightThemeManager.currentTheme = aPersona;
else
Observers.notify("personas:persona:changed");
},
changeToDefaultPersona: function() {
this.selected = "default";
this._prefs.set("persona.lastChanged", new Date().getTime().toString());
this._notifyPersonaChanged(null);
},
changeToRandomPersona: function(category) {
this.category = category;
this.currentPersona = this._getRandomPersonaFromCategory(category);
this.selected = "random";
this._prefs.set("persona.lastChanged", new Date().getTime().toString());
this._notifyPersonaChanged(this.currentPersona);
},
changeToRandomFavoritePersona : function() {
if (this.favorites && this.favorites.length > 0 && this.isUserSignedIn) {
this.currentPersona = this._getRandomPersonaFromArray(this.favorites);
this.selected = "randomFavorite";
this._prefs.set("persona.lastChanged", new Date().getTime().toString());
this._notifyPersonaChanged(this.currentPersona);
}
},
changeToPersona: function(persona) {
// Check whether the persona is in the favorites or the recent lists,
// in which case the change-notification should not be shown.
let recent = this.getRecentPersonas();
let favorites = this.favorites;
let inRecent =
(recent && recent.some(function(v) v.id == persona.id));
let inFavorites =
(favorites && favorites.some(function(v) v.id == persona.id));
this.currentPersona = persona;
this._addPersonaToRecent(persona);
this.selected = "current";
this._prefs.reset("persona.lastRefreshed");
this._prefs.set("persona.lastChanged", new Date().getTime().toString());
this._notifyPersonaChanged(this.currentPersona);
// Show the notification if the selected persona is not in the favorite or
// recent lists, is not a custom persona and its author or username is not null.
// In this case we make sure at least one of these two fields is not null
// to prevent bug 526788.
if (!inRecent && !inFavorites && !persona.custom && (persona.author || persona.username))
this._showPersonaChangeNotification();
},
/**
* Reverts the current persona to the previously selected persona, if
* available
*/
revertToPreviousPersona : function() {
let undonePersonaId = this.currentPersona.id;
let previousPersona = this._prefs.get("lastselected1");
if (previousPersona) {
this.currentPersona = JSON.parse(previousPersona);
this._revertRecent();
this.selected = "current";
if (LightweightThemeManager) {
// forget the lightweight theme too
LightweightThemeManager.forgetUsedTheme(undonePersonaId);
LightweightThemeManager.currentTheme = this.currentPersona;
}
else
this.resetPersona();
}
},
/**
* Shows a notification displaying the currently selected persona and button
* to revert the changes.
*/
_showPersonaChangeNotification : function() {
// Obtain most recent window and its notification box
let wm =
Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
let notificationBox;
switch (this.appInfo.ID) {
case this.FIREFOX_ID:
notificationBox = wm.getMostRecentWindow("navigator:browser").
getBrowser().getNotificationBox();
break;
case this.THUNDERBIRD_ID:
notificationBox = wm.getMostRecentWindow("mail:3pane").
document.getElementById("mail-notification-box");
break;
default:
throw "unknown application ID " + this.appInfo.ID;
}
// If there is another notification of the same kind already, remove it.
let oldNotification =
notificationBox.getNotificationWithValue("lwtheme-install-notification");
if (oldNotification)
notificationBox.removeNotification(oldNotification);
let message = this._strings.get("notification.personaWasSelected",
[this.currentPersona.name,
(this.currentPersona.author ?
this.currentPersona.author :
this.currentPersona.username)]);
let revertButton = {
label : this._strings.get("notification.revertButton.label"),
accesskey : this._strings.get("notification.revertButton.accesskey"),
popup : null,
callback : function() { PersonaService.revertToPreviousPersona(); }
};
notificationBox.appendNotification(
message, "lwtheme-install-notification", null,
notificationBox.PRIORITY_INFO_MEDIUM, [ revertButton ] );
},
/**
* Looks for the given persona in the given array and returns its index.
* @param aPersona The persona to be found.
* @param aPersonaArray The array in which to look for the persona.
* @return The index of the persona in the array; -1 if not found.
*/
_findPersonaInArray : function(aPersona, aPersonaArray) {
for (let i = 0; i < aPersonaArray.length; i++) {
if (aPersonaArray[i].id == aPersona.id)
return i;
}
return -1;
},
_getRandomPersonaFromArray : function(aPersonaArray) {
// Get a random item from the list, trying up to five times to get one
// that is different from the currently-selected item in the category
// (if any). We use Math.floor instead of Math.round to pick a random
// number because the JS reference says Math.round returns a non-uniform
// distribution
// <http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Math:random#Examples>.
if (aPersonaArray && aPersonaArray.length > 0) {
let randomIndex, randomItem;
for (let i = 0; i < 5; i++) {
randomIndex = Math.floor(Math.random() * aPersonaArray.length);
randomItem = aPersonaArray[randomIndex];
if (!this.currentPersona || randomItem.id != this.currentPersona.id)
break;
}
return randomItem;
}
return this.currentPersona;
},
_getRandomPersonaFromCategory: function(aCategoryName) {
// If we have the list of categories, use it to pick a random persona
// from the selected category.
if (this.personas && this.personas.categories) {
let personas;
for each (let category in this.personas.categories) {
if (aCategoryName == category.name) {
personas = category.personas;
break;
}
}
return this._getRandomPersonaFromArray(personas);
}
return this.currentPersona;
},
/**
* Obtains the list of recently selected personas, parsed from the
* "lastselected" preferences.
* @param aHowMany (Optional, default 4) How many personas to obtain.
* @return The list of recent personas.
*/
getRecentPersonas : function(aHowMany) {
if (!aHowMany)
aHowMany = 4;
// Parse the list from the preferences
let personas = [];
for (let i = 0; i < aHowMany; i++) {
if (this._prefs.has("lastselected" + i)) {
try {
personas.push(JSON.parse(this._prefs.get("lastselected" + i)));
}
catch(ex) {}
}
}
return personas;
},
_addPersonaToRecent: function(persona) {
let personas = this.getRecentPersonas();
// Remove personas with the same ID (i.e. don't allow the recent persona
// to appear twice on the list). Afterwards, we'll add the recent persona
// to the list in a way that makes it the most recent one.
if (persona.id)
personas = personas.filter(function(v) !v.id || v.id != persona.id);
// Make the new persona the most recent one.
personas.unshift(persona);
// Note: at this point, there might be five personas on the list, four
// that we parsed from preferences and the one we're now adding. But we
// only serialize the first four back to preferences, so the oldest one
// drops off the end of the list.
// We store five in case we need to revert changes in the list alter on,
// even though only four will be displayed.
for (let i = 0; i < 5; i++) {
if (i < personas.length)
this._prefs.set("lastselected" + i, JSON.stringify(personas[i]));
else
this._prefs.reset("lastselected" + i);
}
},
/**
* Removes the most recent persona from the recent list, and leaves the
* following four personas as the most recent.
*/
_revertRecent: function() {
// Create a new list of recent personas, removing the first one, but
// including the 5th (hidden) one.
let personas = this.getRecentPersonas(5);
personas.shift();
// Serialize the list of recent personas.
for (let i = 0; i < 5; i++) {
if (i < personas.length)
this._prefs.set("lastselected" + i, JSON.stringify(personas[i]));
else
this._prefs.reset("lastselected" + i);
}
},
onUseColorChanged: function() {
// Notify observers that the persona has changed so the change in whether
// or not to use the text or accent color will get applied. The persona
// hasn't really changed, but doing this has the desired effect without any
// known unwanted side effects.
Observers.notify("personas:persona:changed");
},
previewingPersona: null,
/**
* Display the given persona temporarily. Useful for showing users who are
* browsing the directory of personas what a given persona will look like
* when selected, f.e. on mouseover. Consumers who call this method should
* call resetPersona when the preview ends, f.e. on mouseout.
*/
previewPersona: function(persona) {
if (LightweightThemeManager)
LightweightThemeManager.previewTheme(persona);
else {
this.previewingPersona = persona;
Observers.notify("personas:persona:changed");
}
},
/**
* Stop previewing a persona.
*/
resetPersona: function() {
if (LightweightThemeManager)
LightweightThemeManager.resetPreview();
else {
this.previewingPersona = null;
Observers.notify("personas:persona:changed");
}
},
/**
* Gets the persona specified by the initial_persona cookie.
*/
_getPersonaFromCookie: function() {
let authorizedHosts = this._prefs.get("authorizedHosts").split(/[, ]+/);
let cookieManager =
Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
let cookieEnu = cookieManager.enumerator;
let selectedCookie = null;
while (cookieEnu.hasMoreElements()) {
let cookie = cookieEnu.getNext().QueryInterface(Ci.nsICookie);
// XXX: Remove the initial dot from the host name, if any, before it is
// compared against the authorized hosts. This fixes the bug reported in
// bug 492392 that getpersonas.com and www.getpersonas.com cookies
// imported from IE have the cookie host .getpersonas.com, so they didn't
// match any of the authorized hosts.
let cookieHost = cookie.host.replace(/^\./, "");
if (cookie.name == COOKIE_INITIAL_PERSONA &&
authorizedHosts.some(function(v) v == cookieHost)) {
// There could be more than one "initial_persona" cookie. The cookie
// with latest expiration time is selected.
if (null == selectedCookie ||
cookie.expires > selectedCookie.expires) {
selectedCookie = cookie;
}
cookieManager.remove(cookie.host, cookie.name, cookie.path, false);
}
}
if (selectedCookie)
return JSON.parse(decodeURIComponent(selectedCookie.value));
return null;
},
/**
* Whether or not a user is signed in, determined by the presence of a cookie
* named "PERSONA_USER".
* @return True if signed in, false otherwise.
*/
get isUserSignedIn() {
let cookieManager =
Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
let userCookie = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsICookie2, Ci.nsICookie]),
host: this._prefs.get("host"),
path: "/",
name: COOKIE_USER
};
return cookieManager.cookieExists(userCookie);
},
//**************************************************************************//
// Lightweight Themes - Personas synchronization
/**
* Updates the add-on to reflect the changes from the Tools - Add-ons - Themes
* dialog. If a lightweight theme is set, it is also set as the add-on's current
* persona. If a regular theme is set, the current persona is set to "default".
*/
onLightweightThemeChanged: function() {
let currentTheme = LightweightThemeManager.currentTheme;
if (currentTheme && currentTheme.id != this.currentPersona.id)
this.changeToPersona(currentTheme);
else if (!currentTheme && this.selected != "default")
this.changeToDefaultPersona();
},
//**************************************************************************//
// Random Rotation
_rotationTimer : null,
/**
* Checks the current value of the "selected" preference and sets the
* rotation timer accordingly.
*/
onSelectedModeChanged: function() {
this._rotationTimer.cancel();
let selectedMode = this.selected;
if (selectedMode == "random" || selectedMode == "randomFavorite") {
let interval = this._prefs.get("rotationInterval") * 1000;
let that = this;
this._rotationTimer.initWithCallback(
{ notify: function(aTimer) { that._rotatePersona(); } },
interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
this._rotatePersona();
}
},
/**
* Changes the current persona to a random persona of the same category (while
* in "random" mode) or a random persona from he favorite list (while in
* "randomFavorite" mode).
*/
_rotatePersona : function() {
switch (this.selected) {
case "random":
this.changeToRandomPersona(this.category);
break;
case "randomFavorite":
this.changeToRandomFavoritePersona();
break;
}
},
//**************************************************************************//
// Persona Images Caching
/**
* Caches the header and footer images of the given persona inside the
* directory [profile]/personas/cache/[persona.id]. It removes all other
* existing persona directories before doing so.
* @param aPersona The persona for which to cache the images.
*/
_cachePersonaImages : function(aPersona) {
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
// Remove all other subdirectories in the cache directory
// XXX: In the future, if we want to keep more than one persona cached
// this step would be removed.
let subdirs = FileUtils.getDirectoryEntries(cacheDirectory);
for (let i = 0; i < subdirs.length; i++) {
if (subdirs[i].isDirectory())
subdirs[i].remove(true);
}
// Create directory for the given persona
let personaDir = FileUtils.getDirectory(cacheDirectory, aPersona.id);
// Save header if specified.
let header = aPersona.headerURL || aPersona.header;
if (header) {
let headerURI = URI.get(header, null, URI.get(this.dataURL)).
QueryInterface(Ci.nsIURL);
let headerCallback = function(aEvent) {
let request = aEvent.target;
// Save only if the folder still exists (Could have been deleted already)
if (request.status == 200 && personaDir.exists()) {
FileUtils.writeBinaryFile(
personaDir.clone(),
"header" + "." + headerURI.fileExtension,
request.responseText);
}
};
this._makeRequest(headerURI.spec, headerCallback, null, true);
}
// Save footer if specified.
let footer = aPersona.footerURL || aPersona.footer;
if (footer) {
let footerURI = URI.get(footer, null, URI.get(this.dataURL)).
QueryInterface(Ci.nsIURL);
let footerCallback = function(aEvent) {
let request = aEvent.target;
// Save only if the folder still exists (Could have been deleted already)
if (request.status == 200 && personaDir.exists()) {
FileUtils.writeBinaryFile(
personaDir.clone(),
"footer" + "." + footerURI.fileExtension,
request.responseText);
}
};
this._makeRequest(footerURI.spec, footerCallback, null, true);
}
},
/**
* Obtains the cached images of the given persona. This are stored in the
* _cachePersonaImages method under the directory
* [profile]/personas/cache/[persona.id].
* @param aPersona The persona for which to look the cached images.
* @return An object with "header" and "footer" properties, each containing
* the file URL of the image. Null otherwise.
*/
getCachedPersonaImages : function(aPersona) {
let cacheDirectory =
FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
let personaDir = FileUtils.getDirectory(cacheDirectory, aPersona.id, true);
if (personaDir.exists()) {
let headerFile = personaDir.clone();
let footerFile = personaDir.clone();
let headerFileExtension =
URI.get(aPersona.headerURL || aPersona.header, null, URI.get(this.dataURL)).
QueryInterface(Ci.nsIURL).fileExtension;
let footerFileExtension =
URI.get(aPersona.footerURL || aPersona.footer, null, URI.get(this.dataURL)).
QueryInterface(Ci.nsIURL).fileExtension;
headerFile.append("header" + "." + headerFileExtension);
footerFile.append("footer" + "." + footerFileExtension);
if (headerFile.exists() && footerFile.exists()) {
let ios =
Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
let headerURI = ios.newFileURI(headerFile);
let footerURI = ios.newFileURI(footerFile);
return {
header : headerURI.spec,
footer : footerURI.spec
};
}
}
return null;
},
/**
* Monitors changes in cookies. If the modified cookie is the Personas session
* cookie, then the favorites are refreshed (if the user is signed in).
* @param aCookie The cookie that has been added, changed or removed.
*/
onCookieChanged : function(aCookie) {
aCookie.QueryInterface(Ci.nsICookie);
if (aCookie.name == COOKIE_USER &&
aCookie.host == this._prefs.get("host")) {
this.refreshFavorites();
}
},
onQuitApplication: function() {
Observers.remove("quit-application", this.onQuitApplication, this);
this._destroy();
}
};
let DateUtils = {
/**
* Returns the number as a string with a 0 prepended to it if it contains
* only one digit, for formats like ISO 8601 that require two digit month,
* day, hour, minute, and second values (f.e. so midnight on January 1, 2009
* becomes 2009:01:01T00:00:00Z instead of 2009:1:1T0:0:0Z, which would be
* invalid).
*/
_pad: function(number) {
return (number >= 0 && number <= 9) ? "0" + number : "" + number;
},
/**
* Format a date per ISO 8601, in particular the subset described in
* http://www.w3.org/TR/NOTE-datetime, which is recommended for date
* interchange on the internet.
*
* Example: 1994-11-06T08:49:37Z
*
* @param date {Date} the date to format
* @returns {String} the date formatted per ISO 8601
*/
toISO8601: function(date) {
let year = date.getUTCFullYear();
let month = this._pad(date.getUTCMonth() + 1);
let day = this._pad(date.getUTCDate());
let hours = this._pad(date.getUTCHours());
let minutes = this._pad(date.getUTCMinutes());
let seconds = this._pad(date.getUTCSeconds());
return year + "-" + month + "-" + day + "T" +
hours + ":" + minutes + ":" + seconds + "Z";
},
/**
* Format a date per RFC 1123, which is the standard for HTTP headers.
*
* Example: Sun, 06 Nov 1994 08:49:37 GMT
*
* I'd love to use Datejs here, but its Date::toString formatting method
* doesn't convert dates to their UTC equivalents before formatting them,
* resulting in incorrect output (since RFC 1123 requires dates to be
* in UTC), so instead I roll my own.
*
* @param date {Date} the date to format
* @returns {String} the date formatted per RFC 1123
*/
toRFC1123: function(date) {
let dayOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getUTCDay()];
let day = this._pad(date.getUTCDate());
let month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()];
let year = date.getUTCFullYear();
let hours = this._pad(date.getUTCHours());
let minutes = this._pad(date.getUTCMinutes());
let seconds = this._pad(date.getUTCSeconds());
return dayOfWeek + ", " + day + " " + month + " " + year + " " +
hours + ":" + minutes + ":" + seconds + " GMT";
}
};
let FileUtils = {
/**
* Gets the [profile]/personas directory.
* @return The reference to the personas directory (nsIFile).
*/
getPersonasDirectory : function() {
let directoryService =
Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
let dir = directoryService.get("ProfD", Ci.nsIFile);
return this.getDirectory(dir, "personas");
},
/**
* Gets a reference to a directory (nsIFile) specified by the given name,
* located inside the given parent directory.
* @param aParentDirectory The parent directory of the directory to obtain.
* @param aDirectoryName The name of the directory to obtain.
* @param aDontCreate (Optional) Whether or not to create the directory if it
* does not exist.
* @return The reference to the directory (nsIFile).
*/
getDirectory : function(aParentDirectory, aDirectoryName, aDontCreate) {
let dir = aParentDirectory.clone();
try {
dir.append(aDirectoryName);
if (!dir.exists() || !dir.isDirectory()) {
if (!aDontCreate) {
// read and write permissions to owner and group, read-only for others.
dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0774);
}
}
}
catch (ex) {
Cu.reportError("Could not create '" + aDirectoryName + "' directory");
dir = null;
}
return dir;
},
/**
* Gets an array of the entries (nsIFile) found in the given directory.
* @param aDirectory The directory from which to obtain the entries.
* @return The array of entries.
*/
getDirectoryEntries : function(aDirectory) {
let entries = [];
try {
let enu = aDirectory.directoryEntries;
while (enu.hasMoreElements()) {
let entry = enu.getNext().QueryInterface(Ci.nsIFile);
entries.push(entry);
}
}
catch (ex) {
Cu.reportError("Could not read entries of directory");
}
return entries;
},
/**
* Reads the contents of a text file located at the given directory.
* @param aDirectory The directory in which the file is read from (nsIFile)
* @param aFileName The name of the file to be read.
* @return The contents of the file (string), if any.
*/
readFile : function(aDirectory, aFileName) {
let data = "";
try {
let file = aDirectory.clone();
file.append(aFileName);
if (file.exists()) {
let fstream =
Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fstream.init(file, -1, 0, 0);
let cstream =
Cc["@mozilla.org/intl/converter-input-stream;1"].
createInstance(Ci.nsIConverterInputStream);
cstream.init(fstream, "UTF-8", 0, 0);
let (str = {}) {
// read the whole file
while (cstream.readString(-1, str))
data += str.value;
}
cstream.close(); // this also closes fstream
}
}
catch (ex) {
Cu.reportError("Could not read file " + aFileName);
}
return data;
},
/**
* Writes a text file in the given directory. If the file already exists it is
* overwritten.
* @param aDirectory The directory in which the file will be written (nsIFile).
* @param aFileName The name of the file to be written.
* @param aData The contents of the file.
*/
writeFile : function(aDirectory, aFileName, aData) {
try {
let file = aDirectory.clone();
file.append(aFileName);
let foStream =
Cc["@mozilla.org/network/file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
// flags are write, create, truncate
foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
let converter =
Cc["@mozilla.org/intl/converter-output-stream;1"].
createInstance(Ci.nsIConverterOutputStream);
converter.init(foStream, "UTF-8", 0, 0);
converter.writeString(aData);
converter.close(); // this also closes foStream
}
catch (ex) {
Cu.reportError("Could not write file " + aFileName);
}
},
/**
* Writes a binary file in the given directory. If the file already exists it
* is overwritten.
* @param aDirectory The directory in which the file will be written (nsIFile).
* @param aFileName The name of the file to be written.
* @param aData The binary contents of the file.
*/
writeBinaryFile : function(aDirectory, aFileName, aData) {
try {
let file = aDirectory.clone();
file.append(aFileName);
let stream =
Cc["@mozilla.org/network/safe-file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
// Flags are: write, create, truncate
stream.init(file, 0x04 | 0x08 | 0x20, 0600, 0);
stream.write(aData, aData.length);
if (stream instanceof Ci.nsISafeOutputStream)
stream.finish();
else
stream.close();
}
catch (ex) {
Cu.reportError("Could not write binary file " + aFileName);
}
}
};
PersonaService._init();